package contegor.scire.persistence.xml;

import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.datatype.DatatypeFactory;

import contegor.scire.core.Action;
import contegor.scire.core.Concept;
import contegor.scire.core.Connectable;
import contegor.scire.core.Container;
import contegor.scire.core.ContainerImpl;
import contegor.scire.core.DoubleIdFragment;
import contegor.scire.core.DuplicateElementException;
import contegor.scire.core.FloatIdFragment;
import contegor.scire.core.Id;
import contegor.scire.core.IdFragment;
import contegor.scire.core.Identifiable;
import contegor.scire.core.Link;
import contegor.scire.core.LongIdFragment;
import contegor.scire.core.MissingElementException;
import contegor.scire.core.Modifiable;
import contegor.scire.core.IntegerIdFragment;
import contegor.scire.core.Primitive;
import contegor.scire.core.ReturnPoint;
import contegor.scire.core.StringIdFragment;
import contegor.scire.core.Task;
import contegor.scire.core.TimestampIdFragment;
import contegor.scire.core.TrackExternalLinks;
import contegor.scire.core.Transaction;
import contegor.scire.core.TransactionException;
import contegor.scire.core.Version;
import contegor.scire.core.WrongStateException;
import contegor.scire.core.Link.Flow;
import contegor.scire.structure.AbstractConceptCloud;
import contegor.scire.structure.ConceptCloud;

/**
 * XML based {@link ConceptCloud} implementation.
 *
 * @version $Revision$
 * @author  Héctor Cirbián Pousa ($Author$)
 */
@Version("$Id$")
public class XMLConceptCloud extends AbstractConceptCloud {
  private static long CREATION_TIME = System.currentTimeMillis();
  private static Transaction PRIVATE_TRANSACTION = new Transaction(false);

  /**
   * Creates an {@link XMLConceptCloud} with only its root {@link Container} with the given {@link IdFragment} as id.
   */
  public XMLConceptCloud(IdFragment id) throws Exception {
    this.root = new CloudRootContainerImpl(System.currentTimeMillis(), PRIVATE_TRANSACTION.getTransactionId(), id);
    this.commit(PRIVATE_TRANSACTION.nextTaskId());
    check(this.root);
  }

  @SuppressWarnings("unchecked")
  public XMLConceptCloud(InputStream in, ValidationEventHandler veh, ReturnPoint<Void> rp) throws Exception {
    super();
    String packageName = XContainer.class.getPackage().getName();
    JAXBContext jc = JAXBContext.newInstance(packageName);
    Unmarshaller u = jc.createUnmarshaller();
    u.setEventHandler(veh);
    JAXBElement<XContainer> xroot = (JAXBElement<XContainer>)u.unmarshal(in);
    this.root = new CloudRootContainerImpl(System.currentTimeMillis(), PRIVATE_TRANSACTION.getTransactionId(), inferIdFragment(xroot.getValue().getLocalId()));
    List<Task<Void>> trackingTasks = new LinkedList<Task<Void>>();
    addChilds(this.root, xroot.getValue(), trackingTasks);
    for (Task<Void> task: trackingTasks)
      this.root.route(task, rp);
    this.commit(PRIVATE_TRANSACTION.nextTaskId());
    check(this.root);
  }

  private void check(CloudContainerImpl container) throws WrongStateException {
    for (Identifiable child: container.getContent()) {
      if (child instanceof CloudContainerImpl)
        check((CloudContainerImpl)child);
      else if (child instanceof Connectable) 
        check((Connectable)child);
    }
  }

  private void check(Connectable connectable) throws WrongStateException {
    if (connectable instanceof Link) {
      Id targetId = ((Link)connectable).getTarget();
      if (this.root.getId().isAncestorOf(targetId)) {
        Container targetParent = this.find(targetId.getParentId());
        if (targetParent == null)
          throw new WrongStateException("Parent container not found for: " + targetId);
        if (!targetParent.contains(targetId.getLastIdFragment()))
          throw new WrongStateException("Target concept not found: " + targetId);
      }
    }
    for (Link link: connectable.getLinks().values())
      check(link);
  }

  private void addChilds(ContainerImpl container, XContainer xcontainer, List<Task<Void>> trackingTasks) throws DuplicateElementException, WrongStateException, URISyntaxException, TransactionException {
    // Root Container won't have links ...
    for (XIdentifiable xchild: xcontainer.getItem()) {
      IdFragment idf = inferIdFragment(xchild.getLocalId()); 
      if (xchild instanceof XContainer) {
        ContainerImpl child = new CloudContainerImpl(System.currentTimeMillis(), PRIVATE_TRANSACTION.getTransactionId(), idf);
        container.incorporateContainer(child);
        addChilds(child, (XContainer)xchild, trackingTasks);
        // Adding container's links ...
        XConcept xconcept = (XConcept)xchild;
        for (XLink xlink: xconcept.getLink())
          addLink(container, child, xlink, trackingTasks);
      }
      else if (xchild instanceof XConcept) {
        Concept child; 
        if (xchild instanceof XPrimitive)
          child = container.createPrimitive(CREATION_TIME, PRIVATE_TRANSACTION.nextTaskId().getTransactionId(), idf, getPrimitiveValue((XPrimitive)xchild));
        else if (xchild instanceof XAction) {
          XAction xaction = (XAction)xchild;
          child = container.createAction(CREATION_TIME, PRIVATE_TRANSACTION.nextTaskId().getTransactionId(), idf, xaction.getPlugIn(), xaction.getImplementation());
        }
        else if (xchild instanceof XAttribute) {
          XAttribute xattribute = (XAttribute)xchild;
          child = container.createAttribute(CREATION_TIME, PRIVATE_TRANSACTION.nextTaskId().getTransactionId(), idf, xattribute.getGeneratorClass(), xattribute.getGeneratorMethod());
        }
        else
          child = container.createConcept(CREATION_TIME, PRIVATE_TRANSACTION.nextTaskId().getTransactionId(), idf);

        XConcept xconcept = (XConcept)xchild;
        for (XLink xlink: xconcept.getLink())
          addLink(container, child, xlink, trackingTasks);
      }
    }
  }

  private void addLink(Modifiable modifiable, Connectable connectable, XLink xlink, List<Task<Void>> trackingTasks) throws DuplicateElementException, WrongStateException, TransactionException {
    Id targetId = generateId(xlink.getTargetId());
    Link link = modifiable.createLink(CREATION_TIME, PRIVATE_TRANSACTION.nextTaskId().getTransactionId(), Flow.valueOf(xlink.getFlow()), connectable, targetId);
    for (XLink xchildLink: xlink.getLink())
      addLink(modifiable, link, xchildLink, trackingTasks);
    // We need to create tracking messages for links who's targets are in this cloud but in a different container.
    // Targets in other clouds will be tracked when docking with them occurs.
    if (!modifiable.getId().isParentOf(targetId) && modifiable.getId().sameRoot(targetId))
      trackingTasks.add(new TrackExternalLinks(PRIVATE_TRANSACTION.nextTaskId(), targetId.getParentId(), targetId.getLastIdFragment(), modifiable.getId()));
  }

  private IdFragment inferIdFragment(XIdFragment in) {
    if (in instanceof XStringIdFragment)
      return new StringIdFragment(in.getValue());
    if (in instanceof XTimestampIdFragment)
      return new TimestampIdFragment(Long.valueOf(in.getValue()));
    if (in instanceof XIntegerIdFragment)
      return new IntegerIdFragment(Integer.valueOf(in.getValue()));
    if (in instanceof XLongIdFragment)
      return new LongIdFragment(Long.valueOf(in.getValue()));
    if (in instanceof XFloatIdFragment)
      return new FloatIdFragment(Float.valueOf(in.getValue()));
    if (in instanceof XDoubleIdFragment)
      return new DoubleIdFragment(Double.valueOf(in.getValue()));
    throw new IllegalArgumentException("Unknown XIdFragment: " + in);
  }

  private Id generateId(XId xid) {
    List<IdFragment> fragments = new ArrayList<IdFragment>(xid.getFragment().size());
    for (XIdFragment xidf: xid.getFragment())
      fragments.add(inferIdFragment(xidf));
    return new Id(fragments);
  }

  private Object getPrimitiveValue(XPrimitive primitive) throws URISyntaxException {
    if (primitive.isBoolean() != null)
      return primitive.isBoolean();
    if (primitive.getString() != null)
      return primitive.getString();
    if (primitive.getInteger() != null)
      return primitive.getInteger();
    if (primitive.getDecimal() != null)
      return primitive.getDecimal();
    if (primitive.getUri() != null)
      return new URI(primitive.getUri());
    if (primitive.getDateTime() != null)
      return primitive.getDateTime();
    return null;
  }

  @Override
  public <T> void process(Task<T> task, ReturnPoint<? super T> rp) {
    if (this.root.getId().sameRoot(task.getTargetContainerId())) {
      Container container = find(task.getTargetContainerId());
      if (container != null) {
        container.process(task, rp);
        return;
      }
    }
    rp.manage(task.getTaskId(), new MissingElementException("Can't find target container: " + task.getTargetContainerId()));
    rp.finish(task.getTaskId());
  }

  public void save(OutputStream out, ValidationEventHandler veh) throws Exception {
    String packageName = XContainer.class.getPackage().getName();
    JAXBContext jc = JAXBContext.newInstance(packageName);
    Marshaller m = jc.createMarshaller();
    m.setEventHandler(veh);
    ObjectFactory of = new ObjectFactory();
    JAXBElement<XContainer> container = of.createRoot((XContainer)generateXIdentifiable(this.root));
    m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "http://contegor.org/scire cloud.xsd");
    m.marshal(container, out);
  }

  private XIdentifiable generateXIdentifiable(Identifiable identifiable) throws Exception {
    XConcept xconcept;
    if (identifiable instanceof CloudContainerImpl) {
      XContainer xcontainer = new XContainer();
      for (Identifiable child: ((CloudContainerImpl)identifiable).getContent())
        xcontainer.getItem().add(generateXIdentifiable(child));
      xconcept = xcontainer;
    }
    else if (identifiable instanceof Primitive) { 
      XPrimitive xprimitive = new XPrimitive();
      Object value = ((Primitive)identifiable).getValue();
      if (value instanceof Boolean)
        xprimitive.setBoolean((Boolean)value);
      else if (value instanceof String)
        xprimitive.setString((String)value);
      if (value instanceof BigInteger)
        xprimitive.setInteger((BigInteger)value);
      if (value instanceof BigDecimal)
        xprimitive.setDecimal((BigDecimal)value);
      if (value instanceof URI)
        xprimitive.setUri(value.toString());
      if (value instanceof Date) {
        GregorianCalendar now = new GregorianCalendar();
        now.setTime((Date)value);
        xprimitive.setDateTime(DatatypeFactory.newInstance().newXMLGregorianCalendar(now));
      }
      xconcept = xprimitive;
    }
    else if (identifiable instanceof Action) {
      XAction xaction = new XAction();
      xaction.setPlugIn(((Action)identifiable).getPlugIn());
      xaction.setImplementation(((Action)identifiable).getImplementation());
      xconcept = xaction;
    }
    else { // Concept ...
      xconcept = new XConcept();
    }
    xconcept.setLocalId(generateXIdFragment(identifiable.getLocalIdFragment()));
    for (Link link: ((Connectable)identifiable).getLinks().values())
      xconcept.getLink().add(generateXLink(link));
    return xconcept;
  }

  private XLink generateXLink(Link link) {
    XLink xlink = new XLink();
    xlink.setFlow(link.getFlow().name());
    xlink.setTargetId(generateXId(link.getTarget()));
    for (Link child: link.getLinks().values())
      xlink.getLink().add(generateXLink(child));
    return xlink;
  }

  private XId generateXId(Id id) {
    XId xid = new XId();
    for (IdFragment idf: id.getIdFragmentsList())
      xid.getFragment().add(generateXIdFragment(idf));    
    return xid;
  }

  private XIdFragment generateXIdFragment(IdFragment idFragment) {
    XIdFragment result = null;
    if (idFragment instanceof TimestampIdFragment)
      result = new XTimestampIdFragment();
    else if (idFragment instanceof IntegerIdFragment)
      result = new XIntegerIdFragment();
    else if (idFragment instanceof LongIdFragment)
      result = new XLongIdFragment();
    else if (idFragment instanceof FloatIdFragment)
      result = new XFloatIdFragment();
    else if (idFragment instanceof DoubleIdFragment)
      result = new XDoubleIdFragment();
    else
      result = new XStringIdFragment();
    result.setValue(idFragment.toString());
    return result;
  }
}